#!/usr/bin/env python

"""timelog - a desktop widget to help track how you spend your time.

timelog [ help | total | [ day | week | month [ -<N> | <date> ]]]

If invoked without arguments, launch the timelog widget so you can say what
you're doing now.

You know what it does if invoked with the argument help.

If invoked with a single argument of "day", "week" or "month", it reports
on the time logged so far today, this week, or this month. If the single
argument is total, it reports on the entire log.

The optional second argument lets you choose a day/week/month to report on. If
it's "-<N>", then the previous N days/weeks/months are reported. Otherwise,
it's a date of the form"YYYY-MM-DD", and that day, or the week/month
containing that day is reported on.
"""

from sys import argv, exit
from os import chdir, unlink
from os.path import expandvars, join, exists
from string import strip
from time import time as time_overzealous, mktime
from datetime import timedelta, date
from ConfigParser import SafeConfigParser

from Tkinter import Tk, Frame, OptionMenu, Button, StringVar, Label

try:
    set
except NameError:
    # Sigh 2.3? 2.3!? As if not having trinaries was bad enough!
    from sets import Set as set

    def sorted(old, **opts):
        if not opts.has_key('reverse'):
            reverse = False
        else:
            reverse = opts['reverse']
            del opts['reverse']
        new = list(old)
        new.sort(**opts)
        if reverse:
            new.reverse()
        return new
        

# Use this so every reference to off the clock is the same.
off_clock = "Off clock"

def time():
    """Ok, it's cool we can track time to the microsecond. Now don't."""

    return int(time_overzealous())


class Timer(Frame):
    """The Timer object proper. Provides a list of things to log, etc."""

    def __init__(self, logname, activename, projects, **forTk):
        master = forTk['master']

        self.logname = logname
        self.activename = activename
        self.cancel = None
        self.off_clock_color = 'red'
        self.on_clock_color = 'black'

        Frame.__init__(self, **forTk)
        self.pack()

        Button(self, text='Quit', command=master.destroy).pack(side='left')
        self.clock = Label(self, text="00:00:00", fg=self.off_clock_color)
        self.clock.pack(side='left')
        Button(self, text=off_clock, command=self.off_clock).pack(dict(side='left'))

        self.value = StringVar(master)
        self.value.set(off_clock)
        OptionMenu(master, self.value, *projects).pack(side='bottom')

        # And make it run
        if not exists(self.activename):
            self.start = time()
            self.tick()
        else:
            f = open(self.activename)
            self.log(start_time=int(f.read()))
            f.close()

        self.value.trace('w', self.log_wrap)
            
    def destroy(self):
        "If we're going away, go off the clock!"

        self.off_clock()
        Frame.destroy(self)

    def off_clock(self):
        "Off the clock - must be miller time."

        if self.value.get() != off_clock:
            self.value.set(off_clock)
        
    def log_wrap(self, _1, _2, _3, **stuff):
        "Just hide the trace crap from the callback."

        self.log(**stuff)

    def log(self, start_time=None):
        """Log the current value of the options menu to the log file."""

        if start_time:
            self.start = start_time
        else:
            self.start = time()
        out = open(self.logname, 'a')
        val = self.value.get()

        if val == off_clock:
            val = ''
            try:
                unlink(self.activename)
            except OSError:
                pass

        out.write('%s %s\n' % (self.start, val))
        out.close()

        self.tick()

    def tick(self, restart=True):
        """Update various time records one a second."""

        if restart and self.cancel:
            self.clock.after_cancel(self.cancel)

        if self.value.get() == off_clock:
            color = self.off_clock_color
        else:
            color = self.on_clock_color
            out = open(self.activename, 'w')
            out.truncate(0)
            out.write(str(time()))
            out.close()

        self.clock.config(text=str(timedelta(seconds=time() - self.start)),
                          fg=color)

        self.cancel = self.clock.after(1000, self.tick, False)
        

def gui(config):
    """Launch gui using the ConfigParser object config."""

    root = Tk()
    root.title('Timelog')
    app = Timer(config.get('Logger', 'logfile'), config.get('Logger', 'activefile'),
                make_projects(config, make_tasks(config)), master=root)
    if config.has_option('Logger', 'on clock color'):
        app.on_clock_color = config.get('Logger', 'on clock color')
    if config.has_option('Logger', 'off clock color'):
        app.off_clock_color = config.get('Logger', 'off clock color')
    

    app.mainloop()


class Report(dict):
    """Holds a report on activity for a period of time."""

    def __init__(self, logfile, tasks, start, stop):
        """Generate the report for work between start and stop."""

        self.width = 0
        self.total = timedelta()
        self.tasks = tasks

        log = open(logfile)
        ld = ld_old = None
        for line in log:
            ld = line.split()
            if int(ld[0]) >= start:
                break
            ld_old = ld

        if not ld or int(ld[0]) > stop: # nodata
            return
        if ld_old and ld_old[1:]:
            self.add_line_delta(ld_old, timedelta(seconds=int(ld[0]) - start))
        ld_old = ld

        for line in log:
            ld = line.split()
            if int(ld[0]) >= stop:
                self.add_line_delta(ld_old, timedelta(seconds=stop - int(ld_old[0])))
                break
                
            self.add_line_delta(ld_old,
                                timedelta(seconds=int(ld[0]) - int(ld_old[0])))
            ld_old = ld

        if int(ld[0]) < stop and ld[1:]:	# We're not off the clock here!
            self.add_line_delta(ld, timedelta(seconds=int(time() - int(ld[0]))))

        log.close()

    def add_line_delta(self, line, delta):
        """Change the values associated with log line tuple."""

        width = sum(map(len, line), len(line) - 1)
        if width > self.width:
            self.width = width

        line = tuple(line)
        for end in range(2, len(line) + 1):
            key = line[1:end]
            if not self.has_key(key):
                self[key] = delta
            else:
                self[key] += delta
        task = line[-1]
        if task in self.tasks:
            if not self.has_key(task):
                self[task] = delta
            else:
                self[task] += delta

        if line[1:]:
            self.total += delta


def report(config, period, when="current"):
    """Return a dictionary of projects->deltatime objects."""

    count = 1
    if period == 'total':
        startday = date(1900, 1, 1)
        stopday = date.today() + timedelta(days=1)
    else:
        if when[0] in '-c':
            startday = date.today()
        else:
            startday = date(*map(int, when.split('-')))

        if period == 'month':
            # ensues ugliness
            startday = startday.replace(day=1)
            if when[0] == '-':
                count = -int(when)
                year = startday.year + count / 12
                month = count % 12
                if month < startday.month:
                    startday = startday.replace(month=startday.month-month, year=year)
                else:
                    startday = startday.replace(month=12-(month-startday.month),
                                                year=year-1)

            stopday = add_month(startday)
        else:
            if period == 'day':
                period = timedelta(days=1)
            elif period == 'week':
                period = timedelta(days=7)
                delta = startday.weekday() - config.getint('Report', 'weekstart')
                if delta > 0:
                    startday -= timedelta(days=delta)
                else:
                    startday -= timedelta(days=7 + delta)
            else:
                raise ValueError, "Unknown reporting period %s" % period
            

            if when[0] != '-':
                stopday = startday + period
            else:
                count = -int(when)
                startday = startday - period * count
                stopday = startday + period

    tasks = set(make_tasks(config))
    while count:
        print startday, 'to', stopday

        start = int(mktime(startday.timetuple()))
        stop = int(mktime(stopday.timetuple()))

        out = Report(config.get('Logger', 'logfile'), tasks, start, stop)

        if config.get('Report', 'format') == 'fractions':
            for k, v in out.items():
                out[k] = (v.days * 86400 + v.seconds) / 3600.
            out.total = (out.total.days * 86400 + out.total.seconds) / 3600.

        if out:
            for k in sorted(set(out.keys()).difference(tasks), reverse=True):
                print ' '.join(k).ljust(out.width), out[k]
            print

            if tasks:
                print 'Tasks:'
                for k in tasks:
                    if out.has_key(k):
                        print k.ljust(out.width), out[k]
                print
        print 'Total:'.ljust(out.width + 1), out.total

        count -= 1
        if count:
            print
            startday = stopday
            if period != 'month':
                stopday += period
            else:
                stopday = add_month(stopday)
                

def make_projects(config, tasks):
    """Create the list of projects."""
    
    projfile = open(config.get('Logger', 'projectfile'))
    raw = map(strip, projfile.readlines())
    projfile.close()

    if not tasks:
        projects = raw
    else:
        projects = []
        for proj in raw:
            for task in tasks:
                projects.append('%s %s' % (proj, task))
    projects.insert(0, off_clock)
    return projects


def make_tasks(config):
    """Create the list of tasks from the config."""

    if config.has_option('Logger', 'tasks'):
        return config.get('Logger', 'tasks').split()
    return []


def add_month(date):
    if date.month < 12:
        return date.replace(month=date.month + 1)
    else:
        return date.replace(month=1, year=date.year + 1)
        

def main():
    chdir(join(expandvars('$HOME'), '.timelog'))
    conf = SafeConfigParser()
    conf.read('config')
    if len(argv) > 1:
        if argv[1] == 'help':
            print __doc__
            exit()
        report(conf, *argv[1:])
    else:
        gui(conf)
    


if __name__ == '__main__':
    main()
